/*
 * SoapUI, Copyright (C) 2004-2017 SmartBear Software
 *
 * Licensed under the EUPL, Version 1.1 or - as soon as they will be approved by the European Commission - subsequent 
 * versions of the EUPL (the "Licence"); 
 * You may not use this work except in compliance with the Licence. 
 * You may obtain a copy of the Licence at: 
 * 
 * http://ec.europa.eu/idabc/eupl 
 * 
 * Unless required by applicable law or agreed to in writing, software distributed under the Licence is 
 * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either 
 * express or implied. See the Licence for the specific language governing permissions and limitations 
 * under the Licence. 
 */

package com.eviware.soapui.support.dnd;

import com.eviware.soapui.SoapUI;
import com.eviware.soapui.model.ModelItem;
import com.eviware.soapui.support.dnd.handlers.InterfaceToProjectDropHandler;
import com.eviware.soapui.support.dnd.handlers.MockResponseToTestCaseDropHandler;
import com.eviware.soapui.support.dnd.handlers.MockResponseToTestStepDropHandler;
import com.eviware.soapui.support.dnd.handlers.MockResponseToTestStepsDropHandler;
import com.eviware.soapui.support.dnd.handlers.MockServiceToProjectDropHandler;
import com.eviware.soapui.support.dnd.handlers.OperationToMockServiceDropHandler;
import com.eviware.soapui.support.dnd.handlers.RequestToMockOperationDropHandler;
import com.eviware.soapui.support.dnd.handlers.RequestToTestCaseDropHandler;
import com.eviware.soapui.support.dnd.handlers.RequestToTestStepDropHandler;
import com.eviware.soapui.support.dnd.handlers.RequestToTestStepsDropHandler;
import com.eviware.soapui.support.dnd.handlers.TestCaseToProjectDropHandler;
import com.eviware.soapui.support.dnd.handlers.TestCaseToTestCaseDropHandler;
import com.eviware.soapui.support.dnd.handlers.TestCaseToTestSuiteDropHandler;
import com.eviware.soapui.support.dnd.handlers.TestStepToTestCaseDropHandler;
import com.eviware.soapui.support.dnd.handlers.TestStepToTestStepDropHandler;
import com.eviware.soapui.support.dnd.handlers.TestStepToTestStepsDropHandler;
import com.eviware.soapui.support.dnd.handlers.TestSuiteToProjectDropHandler;
import com.eviware.soapui.support.dnd.handlers.TestSuiteToTestSuiteDropHandler;

import javax.swing.Timer;
import javax.swing.ToolTipManager;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Component;
import java.awt.GradientPaint;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.SystemColor;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.dnd.DnDConstants;
import java.awt.dnd.DragGestureEvent;
import java.awt.dnd.DragGestureListener;
import java.awt.dnd.DragSource;
import java.awt.dnd.DragSourceDragEvent;
import java.awt.dnd.DragSourceDropEvent;
import java.awt.dnd.DragSourceEvent;
import java.awt.dnd.DragSourceListener;
import java.awt.dnd.DropTarget;
import java.awt.dnd.DropTargetDragEvent;
import java.awt.dnd.DropTargetDropEvent;
import java.awt.dnd.DropTargetEvent;
import java.awt.dnd.DropTargetListener;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.List;

public class SoapUIDragAndDropHandler implements DragGestureListener, DragSourceListener {
    public static final int ON_RANGE = 3;
    private final SoapUIDragAndDropable<ModelItem> dragAndDropable;
    private BufferedImage _imgGhost; // The 'drag image'
    private Point _ptOffset = new Point(); // Where, in the drag image, the mouse
    private static List<ModelItemDropHandler<ModelItem>> handlers;
    private Rectangle2D _raGhost = new Rectangle2D.Float();
    private final int dropType;
    private Point _ptLast = new Point();

    static {
        handlers = new ArrayList<ModelItemDropHandler<ModelItem>>();
        SoapUIDragAndDropHandler.addDropHandler(new TestStepToTestCaseDropHandler());
        SoapUIDragAndDropHandler.addDropHandler(new TestStepToTestStepsDropHandler());
        SoapUIDragAndDropHandler.addDropHandler(new TestStepToTestStepDropHandler());
        SoapUIDragAndDropHandler.addDropHandler(new TestSuiteToProjectDropHandler());
        SoapUIDragAndDropHandler.addDropHandler(new InterfaceToProjectDropHandler());
        SoapUIDragAndDropHandler.addDropHandler(new TestCaseToProjectDropHandler());
        SoapUIDragAndDropHandler.addDropHandler(new TestCaseToTestSuiteDropHandler());
        SoapUIDragAndDropHandler.addDropHandler(new TestCaseToTestCaseDropHandler());
        SoapUIDragAndDropHandler.addDropHandler(new RequestToTestCaseDropHandler());
        SoapUIDragAndDropHandler.addDropHandler(new RequestToTestStepsDropHandler());
        SoapUIDragAndDropHandler.addDropHandler(new RequestToTestStepDropHandler());
        SoapUIDragAndDropHandler.addDropHandler(new RequestToMockOperationDropHandler());
        SoapUIDragAndDropHandler.addDropHandler(new MockServiceToProjectDropHandler());
        SoapUIDragAndDropHandler.addDropHandler(new OperationToMockServiceDropHandler());
        SoapUIDragAndDropHandler.addDropHandler(new MockResponseToTestCaseDropHandler());
        SoapUIDragAndDropHandler.addDropHandler(new MockResponseToTestStepDropHandler());
        SoapUIDragAndDropHandler.addDropHandler(new MockResponseToTestStepsDropHandler());
        SoapUIDragAndDropHandler.addDropHandler(new TestSuiteToTestSuiteDropHandler());
    }

    @SuppressWarnings("unchecked")
    public SoapUIDragAndDropHandler(SoapUIDragAndDropable target, int dropType) {
        this.dragAndDropable = target;
        this.dropType = dropType;

        // Also, make this JTree a drag target
        DropTarget dropTarget = new DropTarget(target.getComponent(), new SoapUIDropTargetListener());
        dropTarget.setDefaultActions(DnDConstants.ACTION_COPY_OR_MOVE);
    }

    @SuppressWarnings("unchecked")
    public static void addDropHandler(ModelItemDropHandler dropHandler) {
        handlers.add(dropHandler);
    }

    public void dragGestureRecognized(DragGestureEvent e) {
        Point ptDragOrigin = e.getDragOrigin();
        ModelItem modelItem = dragAndDropable.getModelItemForLocation(ptDragOrigin.x, ptDragOrigin.y);
        if (modelItem == null) {
            return;
        }

        Rectangle raPath = dragAndDropable.getModelItemBounds(modelItem);
        if (raPath == null) {
            return;
        }

        _ptOffset = new Point(ptDragOrigin.x - raPath.x, ptDragOrigin.y - raPath.y);

        Component renderer = dragAndDropable.getRenderer(modelItem);
        if (renderer != null) {
            renderer.setSize((int) raPath.getWidth(), (int) raPath.getHeight()); // <--

            // Get a buffered image of the selection for dragging a ghost image
            _imgGhost = new BufferedImage((int) raPath.getWidth(), (int) raPath.getHeight(),
                    BufferedImage.TYPE_INT_ARGB_PRE);
            Graphics2D g2 = _imgGhost.createGraphics();

            // Ask the cell renderer to paint itself into the BufferedImage
            g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC, 0.5f));
            renderer.paint(g2);

            g2.setComposite(AlphaComposite.getInstance(AlphaComposite.DST_OVER, 0.5f));

            int width = dragAndDropable.getComponent().getWidth();
            g2.setPaint(new GradientPaint(0, 0, SystemColor.controlShadow, width, 0, new Color(255, 255, 255, 0)));
            g2.fillRect(0, 0, width, _imgGhost.getHeight());
            g2.dispose();
        } else {
            _imgGhost = null;
        }

        dragAndDropable.selectModelItem(modelItem); // Select this path in the
        // tree

        // Wrap the path being transferred into a Transferable object
        Transferable transferable = new ModelItemTransferable(modelItem);

        // We pass our drag image just in case it IS supported by the platform
        e.startDrag(null, _imgGhost, new Point(5, 5), transferable, this);
    }

    public void dragDropEnd(DragSourceDropEvent dsde) {
        if (_raGhost != null) {
            dragAndDropable.getComponent().repaint(_raGhost.getBounds());
        }

        _ptOffset = null;
        SoapUI.getNavigator().getMainTree().setToolTipText(null);
    }

    public void dragEnter(DragSourceDragEvent dsde) {

    }

    public void dragExit(DragSourceEvent dse) {

    }

    public void dragOver(DragSourceDragEvent dsde) {
    }

    public void dropActionChanged(DragSourceDragEvent dsde) {
    }

    // DropTargetListener interface object...
    class SoapUIDropTargetListener implements DropTargetListener {
        // Fields...
        private ModelItem _pathLast = null;
        private Rectangle2D _raCueLine = new Rectangle2D.Float();
        private Color _colorCueLine;
        private Timer _timerHover;
        // private int _nLeftRight = 0; // Cumulative left/right mouse movement
        private String dropInfo;

        // Constructor...
        public SoapUIDropTargetListener() {
            _colorCueLine = new Color(SystemColor.controlShadow.getRed(), SystemColor.controlShadow.getGreen(),
                    SystemColor.controlShadow.getBlue(), 128);

            // Set up a hover timer, so that a node will be automatically expanded
            _timerHover = new Timer(1000, new ActionListener() {
                public void actionPerformed(ActionEvent e) {
                    if (_ptOffset != null) {
                        dragAndDropable.toggleExpansion(_pathLast);
                    }
                }
            });
            _timerHover.setRepeats(false); // Set timer to one-shot mode
        }

        // DropTargetListener interface
        public void dragEnter(DropTargetDragEvent e) {
            int dt = getDropTypeAtPoint(e.getLocation());

            if (dt == DropType.NONE || !isDragAcceptable(e, dt)) {
                e.rejectDrag();
            } else {
                e.acceptDrag(e.getDropAction());
            }
        }

        private int getDropTypeAtPoint(Point pt) {
            ModelItem modelItem = dragAndDropable.getModelItemForLocation(pt.x, pt.y);
            if (modelItem == null) {
                return DropType.NONE;
            }

            Rectangle raPath = dragAndDropable.getModelItemBounds(modelItem);

            if (pt.y > (raPath.y + (raPath.getHeight() / 2) + ON_RANGE)) {
                return DropType.AFTER;
            } else if (pt.y < (raPath.y + (raPath.getHeight() / 2) - ON_RANGE)) {
                return DropType.BEFORE;
            } else {
                return DropType.ON;
            }
        }

        public void dragExit(DropTargetEvent e) {
            if (!DragSource.isDragImageSupported()) {
                dragAndDropable.getComponent().repaint(_raGhost.getBounds());
            }
        }

        /**
         * This is where the ghost image is drawn
         */
        public void dragOver(DropTargetDragEvent e) {
            // Even if the mouse is not moving, this method is still invoked 10
            // times per second
            Point pt = e.getLocation();
            if (pt.equals(_ptLast)) {
                return;
            }

            _ptLast = pt;

            Graphics2D g2 = (Graphics2D) dragAndDropable.getComponent().getGraphics();

            // If a drag image is not supported by the platform, then draw my own
            // drag image
            if (!DragSource.isDragImageSupported() && _imgGhost != null && _ptOffset != null) {
                dragAndDropable.getComponent().paintImmediately(_raGhost.getBounds()); // Rub
                // out
                // the
                // last ghost
                // image and cue line
                // And remember where we are about to draw the new ghost image
                _raGhost.setRect(pt.x - _ptOffset.x, pt.y - _ptOffset.y, _imgGhost.getWidth(), _imgGhost.getHeight());
                g2.drawImage(_imgGhost, AffineTransform.getTranslateInstance(_raGhost.getX(), _raGhost.getY()), null);
            } else
            // Just rub out the last cue line
            {
                dragAndDropable.getComponent().paintImmediately(_raCueLine.getBounds());
            }

            ModelItem modelItem = dragAndDropable.getModelItemForLocation(pt.x, pt.y);
            if (modelItem == null) {
                e.rejectDrag();
                return;
            }

            if (!(modelItem == _pathLast)) {
                // movement trend
                _pathLast = modelItem;
                _timerHover.restart();
            }

            // In any case draw (over the ghost image if necessary) a cue line
            // indicating where a drop will occur
            Rectangle raPath = dragAndDropable.getModelItemBounds(modelItem);

            int dt = dropType;

            if (dropType == DropType.AFTER) {
                _raCueLine.setRect(0, raPath.y + (int) raPath.getHeight() - 2, dragAndDropable.getComponent().getWidth(),
                        2);
            } else if (dropType == DropType.BEFORE) {
                _raCueLine.setRect(0, raPath.y, dragAndDropable.getComponent().getWidth(), 2);
            } else if (dropType == DropType.ON) {
                _raCueLine.setRect(0, raPath.y, dragAndDropable.getComponent().getWidth(), raPath.getHeight());
            } else {
                if (pt.y > (raPath.y + (raPath.getHeight() / 2) + ON_RANGE)) {
                    _raCueLine.setRect(0, raPath.y + (int) raPath.getHeight() - 2, dragAndDropable.getComponent()
                            .getWidth(), 2);
                    dt = DropType.AFTER;
                } else if (pt.y < (raPath.y + (raPath.getHeight() / 2) - ON_RANGE)) {
                    _raCueLine.setRect(0, raPath.y, dragAndDropable.getComponent().getWidth(), 2);
                    dt = DropType.BEFORE;
                } else {
                    _raCueLine.setRect(0, raPath.y, dragAndDropable.getComponent().getWidth(), raPath.getHeight());
                    dt = DropType.ON;
                }
            }

            boolean dragAcceptable = isDragAcceptable(e, dt);
            g2.setColor(_colorCueLine);
            g2.fill(_raCueLine);

            if (dragAcceptable) {
                dragAndDropable.setDragInfo(dropInfo);
            } else {
                dragAndDropable.setDragInfo("");
            }

            ToolTipManager.sharedInstance().mouseMoved(
                    new MouseEvent(dragAndDropable.getComponent(), 0, 0, 0, pt.x, pt.y + 10, // X-Y
                            // of
                            // the
                            // mouse
                            // for
                            // the
                            // tool
                            // tip
                            0, false));

            // And include the cue line in the area to be rubbed out next time
            _raGhost = _raGhost.createUnion(_raCueLine);

            if (!dragAcceptable) {
                e.rejectDrag();
            } else {
                e.acceptDrag(e.getDropAction());
            }
        }

        public void dropActionChanged(DropTargetDragEvent e) {
            int dt = getDropTypeAtPoint(e.getLocation());

            if (dt == DropType.NONE || !isDragAcceptable(e, dt)) {
                e.rejectDrag();
            } else {
                e.acceptDrag(e.getDropAction());
            }
        }

        public void drop(DropTargetDropEvent e) {
            int dt = getDropTypeAtPoint(e.getLocation());
            _timerHover.stop();

            if (dt == DropType.NONE || !isDropAcceptable(e, dt)) {
                e.rejectDrop();
                return;
            }

            e.acceptDrop(e.getDropAction());

            Transferable transferable = e.getTransferable();

            DataFlavor[] flavors = transferable.getTransferDataFlavors();
            for (int i = 0; i < flavors.length; i++) {
                DataFlavor flavor = flavors[i];
                if (flavor.isMimeTypeEqual(DataFlavor.javaJVMLocalObjectMimeType)) {
                    try {
                        Point pt = e.getLocation();
                        ModelItem pathTarget = dragAndDropable.getModelItemForLocation(pt.x, pt.y);
                        ModelItem pathSource = (ModelItem) transferable.getTransferData(flavor);

                        for (ModelItemDropHandler<ModelItem> handler : handlers) {
                            if (handler.canDrop(pathSource, pathTarget, e.getDropAction(), dt)) {
                                // System.out.println( "Got drop handler for " +
                                // pathSource.getName() + " to " + pathTarget.getName()
                                // + "; " + handler.getClass().getSimpleName() );

                                handler.drop(pathSource, pathTarget, e.getDropAction(), dt);
                                break;
                            }
                        }

                        break; // No need to check remaining flavors
                    } catch (Exception ioe) {
                        System.out.println(ioe);
                        e.dropComplete(false);
                        return;
                    }
                }
            }

            e.dropComplete(true);
        }

        // Helpers...
        public boolean isDragAcceptable(DropTargetDragEvent e, int dt) {
            // Only accept COPY or MOVE gestures (ie LINK is not supported)
            if ((e.getDropAction() & DnDConstants.ACTION_COPY_OR_MOVE) == 0) {
                return false;
            }

            // Only accept this particular flavor
            if (!e.isDataFlavorSupported(ModelItemTransferable.MODELITEM_DATAFLAVOR)) {
                return false;
            }

            Transferable transferable = e.getTransferable();

            DataFlavor[] flavors = transferable.getTransferDataFlavors();
            for (int i = 0; i < flavors.length; i++) {
                DataFlavor flavor = flavors[i];
                if (flavor.isMimeTypeEqual(DataFlavor.javaJVMLocalObjectMimeType)) {
                    try {
                        Point pt = e.getLocation();
                        ModelItem pathTarget = dragAndDropable.getModelItemForLocation(pt.x, pt.y);
                        ModelItem pathSource = (ModelItem) transferable.getTransferData(flavor);

                        for (ModelItemDropHandler<ModelItem> handler : handlers) {
                            if (handler.canDrop(pathSource, pathTarget, e.getDropAction(), dt)) {
                                dropInfo = handler.getDropInfo(pathSource, pathTarget, e.getDropAction(), dt);
                                // System.out.println( "Got drag handler for " +
                                // pathSource.getName() + " to " + pathTarget.getName()
                                // + "; " + handler.getClass().getSimpleName() );
                                return true;
                            }
                        }

                        // System.out.println( "Missing drop handler for " +
                        // pathSource.getName() + " to " + pathTarget.getName() );

                        dropInfo = null;
                    } catch (Exception ex) {
                        SoapUI.logError(ex);
                    }
                }
            }

            return false;
        }

        public boolean isDropAcceptable(DropTargetDropEvent e, int dt) {
            // Only accept COPY or MOVE gestures (ie LINK is not supported)
            if ((e.getDropAction() & DnDConstants.ACTION_COPY_OR_MOVE) == 0) {
                return false;
            }

            // Only accept this particular flavor
            if (!e.isDataFlavorSupported(ModelItemTransferable.MODELITEM_DATAFLAVOR)) {
                return false;
            }

            Transferable transferable = e.getTransferable();

            DataFlavor[] flavors = transferable.getTransferDataFlavors();
            for (int i = 0; i < flavors.length; i++) {
                DataFlavor flavor = flavors[i];
                if (flavor.isMimeTypeEqual(DataFlavor.javaJVMLocalObjectMimeType)) {
                    try {
                        Point pt = e.getLocation();
                        ModelItem pathSource = (ModelItem) transferable.getTransferData(flavor);
                        ModelItem pathTarget = dragAndDropable.getModelItemForLocation(pt.x, pt.y);

                        for (ModelItemDropHandler<ModelItem> handler : handlers) {
                            if (handler.canDrop(pathSource, pathTarget, e.getDropAction(), dt)) {
                                return true;
                            }
                        }
                    } catch (Exception ex) {
                        SoapUI.logError(ex);
                    }
                }
            }

            return false;
        }
    }
}
